package ru.classbase.dbdumper

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.springframework.stereotype.Service
import ru.classbase.dbdumper.model.*
import ru.classbase.dbdumper.storage.DumpStorage
import ru.classbase.dbdumper.writer.DumpWriter
import java.io.*
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import javax.sql.DataSource

@Service
class DumpManager(
    private val dataSource: DataSource,
    private val props: AppProperties,
    private val dumpers: List<Dumper>,
    private val writers: List<DumpWriter>,
    private val storages: List<DumpStorage>,
) {
    private val logger = logger()

    @ExperimentalCoroutinesApi
    fun run() {
        validateProperties(props)

        logger.info("Extracting database information...")
        val databaseType = getDatabaseProduct()

        logger.info("Find handler...")
        val dumper = findDumper(databaseType)
        val storage = findStorage(props.storageType)

        val meta = dumper.getMetaData()

        logger.info("Lock tables...")

        dumper.lockTables(meta).use { _ ->

            logger.info("Tables successfully locked")

            runBlocking {
                meta.schemas.forEach { (schemaName, schema) ->
                    schema.tables.forEach { (tableName, _) ->
                        val input = PipedInputStream()
                        val output = getOutputStream(input)

                        if(output is ZipOutputStream) {
                            output.putNextEntry(ZipEntry("$schemaName.$tableName.${props.outputFormat.ext}"))
                        }

                        val storageContext = StorageContext(schemaName, tableName, input)

                        launch(Dispatchers.IO.limitedParallelism(props.workerCount)) {
                            try {
                                output.use {
                                    dump(schemaName, tableName, meta, output)
                                    logger.info("Данные извлечены, schema: {}, table: {}", schemaName, tableName)
                                }

                            }
                            catch (e : Exception) {
                                logger.error("Ошибка извлечения данных: schema: {}, table: {}", schemaName, tableName, e)
                            }
                        }

                        launch(Dispatchers.IO.limitedParallelism(props.workerCount)) {
                            try {
                                input.use {
                                    storage.uploadFrom(meta, storageContext)
                                    logger.info("Данные сохранены, schema: {}, tableName: {}", schemaName, tableName)
                                }
                            }
                            catch (e : Exception) {
                                logger.error("Ошибка сохранения данных: schema: {}, table: {}", schemaName, tableName, e)
                            }
                        }
                    }
                }
            }
        }

    }

    fun dump(schema: String, tableName: String, meta: DbMetaData, output: OutputStream) {
        logger.info("Извлечение данных, schema={}, table={}", schema, tableName)

        val columns = meta.schemas[schema]!!.tables[tableName]!!.columns.map { it.value }

        val joined = columns.joinToString(",") { it.columnName }

        val sql = "select $joined from $schema.$tableName"

        val dumpWriter = findDumpWriter(props.outputFormat)

        dataSource.connection.use { conn ->
            conn.prepareStatement(sql).executeQuery().use { rs ->

                val writer = BufferedWriter(OutputStreamWriter(output, props.outputEncoding), 1024 * 1024)

                writer.use {

                    val dumpContext = DumpContext(schema, tableName, columns, rs, writer)
                    dumpWriter.write(meta, dumpContext)
                }
            }
        }

    }

    private fun findDumpWriter(format: OutputFormat): DumpWriter {
        return writers.find { it.supportedFormat == format }
            ?: throw IllegalArgumentException("Format not supported: $format")
    }

    private fun findDumper(product: DatabaseProduct): Dumper {
        return dumpers.find { it.supportedDatabase == product }
            ?: throw IllegalArgumentException("Database product not supported: $product")
    }

    private fun findStorage(storageType: StorageType): DumpStorage {
        return storages.find { it.supportedStorage == storageType }
            ?: throw IllegalArgumentException("Storage not supported: $storageType")
    }

    private fun getDatabaseProduct(): DatabaseProduct {
        return dataSource.connection.use { conn ->
            val metaData = conn.metaData

            logger.info("Database product name: {}", metaData.databaseProductName)
            logger.info("Database major version: {}", metaData.databaseMajorVersion)
            logger.info("Database product version: {}", metaData.databaseProductVersion)

            DatabaseProduct.find(metaData.databaseProductName)
        }

    }

    private fun getOutputStream(input: PipedInputStream): OutputStream {
        val pipedOutputStream = PipedOutputStream(input)

        if(props.compressionType == CompressionType.NONE) {
            return pipedOutputStream
        }

        return ZipOutputStream(pipedOutputStream )
    }


    /*
    */

}